/*
 * $Id: TarOutputStream.java,v 1.2 2009/07/24 18:28:04 oliver Exp $
 *
 * Copyright (C) 2006 General Electric Company. All Rights Reserved.
 *
 * This software is the confidential and proprietary information of the General
 * Electric Company (GE). You shall not disclose this software and shall use it
 * only in accordance with the terms of the license agreement you entered into
 * with GE.
 *
 * GE MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
 * NON-INFRINGEMENT. GE SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING, OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES.
 */

/*
 ** Authored by Timothy Gerard Endres
 ** <mailto:time@ice.com>  <http://www.ice.com>
 ** 
 ** This work has been placed into the public domain.
 ** You may use this work in any way and for any purpose you wish.
 **
 ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
 ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
 ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
 ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
 ** REDISTRIBUTION OF THIS SOFTWARE. 
 ** 
 */

package com.ge.healthcare.autosc.common.util.tar;

import java.io.*;

//import javax.activation.*;

/**
 * The TarOutputStream writes a UNIX tar archive as an OutputStream. Methods are
 * provided to put entries, and then write their contents by writing to this
 * stream using write().
 * 
 * 
 * @version $Revision: 1.2 $
 * @author Timothy Gerard Endres, <a
 *         href="mailto:time@ice.com">time@ice.com</a>.
 * @see TarBuffer
 * @see TarHeader
 * @see TarEntry
 */

public class TarOutputStream extends FilterOutputStream {
	protected boolean debug;
	protected int currSize;
	protected int currBytes;
	protected byte[] oneBuf;
	protected byte[] recordBuf;
	protected int assemLen;
	protected byte[] assemBuf;
	protected TarBuffer buffer;

	public TarOutputStream(OutputStream os) {
		this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
	}

	public TarOutputStream(OutputStream os, int blockSize) {
		this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE);
	}

	public TarOutputStream(OutputStream os, int blockSize, int recordSize) {
		super(os);

		this.buffer = new TarBuffer(os, blockSize, recordSize);

		this.debug = false;
		this.assemLen = 0;
		this.assemBuf = new byte[recordSize];
		this.recordBuf = new byte[recordSize];
		this.oneBuf = new byte[1];
	}

	/**
	 * Sets the debugging flag.
	 * 
	 * @param debugF
	 *            True to turn on debugging.
	 */
	public void setDebug(boolean debugF) {
		this.debug = debugF;
	}

	/**
	 * Sets the debugging flag in this stream's TarBuffer.
	 * 
	 * @param debugF
	 *            True to turn on debugging.
	 */
	public void setBufferDebug(boolean debug) {
		this.buffer.setDebug(debug);
	}

	/**
	 * Ends the TAR archive without closing the underlying OutputStream. The
	 * result is that the EOF record of nulls is written.
	 */

	public void finish() throws IOException {
		this.writeEOFRecord();
	}

	/**
	 * Ends the TAR archive and closes the underlying OutputStream. This means
	 * that finish() is called followed by calling the TarBuffer's close().
	 */

	public void close() throws IOException {
		this.finish();
		this.buffer.close();
	}

	/**
	 * Get the record size being used by this stream's TarBuffer.
	 * 
	 * @return The TarBuffer record size.
	 */
	public int getRecordSize() {
		return this.buffer.getRecordSize();
	}

	/**
	 * Put an entry on the output stream. This writes the entry's header record
	 * and positions the output stream for writing the contents of the entry.
	 * Once this method is called, the stream is ready for calls to write() to
	 * write the entry's contents. Once the contents are written, closeEntry()
	 * <B>MUST</B> be called to ensure that all buffered data is completely
	 * written to the output stream.
	 * 
	 * @param entry
	 *            The TarEntry to be written to the archive.
	 */
	public void putNextEntry(TarEntry entry) throws IOException {
		if (entry.getHeader().name.length() > TarHeader.NAMELEN)
			throw new InvalidHeaderException("file name '"
					+ entry.getHeader().name + "' is too long ( > "
					+ TarHeader.NAMELEN + " bytes )");

		entry.writeEntryHeader(this.recordBuf);
		this.buffer.writeRecord(this.recordBuf);

		this.currBytes = 0;

		if (entry.isDirectory())
			this.currSize = 0;
		else
			this.currSize = (int) entry.getSize();
	}

	/**
	 * Close an entry. This method MUST be called for all file entries that
	 * contain data. The reason is that we must buffer data written to the
	 * stream in order to satisfy the buffer's record based writes. Thus, there
	 * may be data fragments still being assembled that must be written to the
	 * output stream before this entry is closed and the next entry written.
	 */
	public void closeEntry() throws IOException {
		if (this.assemLen > 0) {
			for (int i = this.assemLen; i < this.assemBuf.length; ++i)
				this.assemBuf[i] = 0;

			this.buffer.writeRecord(this.assemBuf);

			this.currBytes += this.assemLen;
			this.assemLen = 0;
		}

		if (this.currBytes < this.currSize)
			throw new IOException("entry closed at '" + this.currBytes
					+ "' before the '" + this.currSize
					+ "' bytes specified in the header were written");
	}

	/**
	 * Writes a byte to the current tar archive entry.
	 * 
	 * This method simply calls read( byte[], int, int ).
	 * 
	 * @param b
	 *            The byte written.
	 */
	public void write(int b) throws IOException {
		this.oneBuf[0] = (byte) b;
		this.write(this.oneBuf, 0, 1);
	}

	/**
	 * Writes bytes to the current tar archive entry.
	 * 
	 * This method simply calls read( byte[], int, int ).
	 * 
	 * @param wBuf
	 *            The buffer to write to the archive.
	 * @return The number of bytes read, or -1 at EOF.
	 */
	public void write(byte[] wBuf) throws IOException {
		this.write(wBuf, 0, wBuf.length);
	}

	/**
	 * Writes bytes to the current tar archive entry. This method is aware of
	 * the current entry and will throw an exception if you attempt to write
	 * bytes past the length specified for the current entry. The method is also
	 * (painfully) aware of the record buffering required by TarBuffer, and
	 * manages buffers that are not a multiple of recordsize in length,
	 * including assembling records from small buffers.
	 * 
	 * This method simply calls read( byte[], int, int ).
	 * 
	 * @param wBuf
	 *            The buffer to write to the archive.
	 * @param wOffset
	 *            The offset in the buffer from which to get bytes.
	 * @param numToWrite
	 *            The number of bytes to write.
	 */
	public void write(byte[] wBuf, int wOffset, int numToWrite)
			throws IOException {
		if ((this.currBytes + numToWrite) > this.currSize)
			throw new IOException("request to write '" + numToWrite
					+ "' bytes exceeds size in header of '" + this.currSize
					+ "' bytes");

		//
		// We have to deal with assembly!!!
		// The programmer can be writing little 32 byte chunks for all
		// we know, and we must assemble complete records for writing.
		// REVIEW Maybe this should be in TarBuffer? Could that help to
		// eliminate some of the buffer copying.
		//
		if (this.assemLen > 0) {
			if ((this.assemLen + numToWrite) >= this.recordBuf.length) {
				int aLen = this.recordBuf.length - this.assemLen;

				System.arraycopy(this.assemBuf, 0, this.recordBuf, 0,
						this.assemLen);

				System.arraycopy(wBuf, wOffset, this.recordBuf, this.assemLen,
						aLen);

				this.buffer.writeRecord(this.recordBuf);

				this.currBytes += this.recordBuf.length;

				wOffset += aLen;
				numToWrite -= aLen;
				this.assemLen = 0;
			} else // ( (this.assemLen + numToWrite ) < this.recordBuf.length )
			{
				System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen,
						numToWrite);
				wOffset += numToWrite;
				this.assemLen += numToWrite;
				numToWrite -= numToWrite;
			}
		}

		//
		// When we get here we have EITHER:
		// o An empty "assemble" buffer.
		// o No bytes to write (numToWrite == 0)
		//

		for (; numToWrite > 0;) {
			if (numToWrite < this.recordBuf.length) {
				System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen,
						numToWrite);
				this.assemLen += numToWrite;
				break;
			}

			this.buffer.writeRecord(wBuf, wOffset);

			int num = this.recordBuf.length;
			this.currBytes += num;
			numToWrite -= num;
			wOffset += num;
		}
	}

	/**
	 * Write an EOF (end of archive) record to the tar archive. An EOF record
	 * consists of a record of all zeros.
	 */
	private void writeEOFRecord() throws IOException {
		for (int i = 0; i < this.recordBuf.length; ++i)
			this.recordBuf[i] = 0;
		this.buffer.writeRecord(this.recordBuf);
	}

}
